package edu.ssdut.revise.core.match;

import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.utils.Pair;
import edu.ssdut.revise.exceptions.NotExistOrNotDirectoryException;
import edu.ssdut.revise.exceptions.ParserException;
import edu.ssdut.revise.models.CUWithInfo;
import edu.ssdut.revise.utils.FileUtils;
import edu.ssdut.revise.utils.javaParserUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * For differing between directories. <br/>
 * Match files, containing two source directories.<br/>
 * <b>Input:</b> directory paths.<br/>
 * <b>Output:</b> matched files.
 */
public class CUMatcher {
    private final static Logger logger = LoggerFactory.getLogger(CUMatcher.class);

    MatchStore<CUWithInfo> cuMatchStore = new MatchStore<>();
    private final File rootDir1;
    private final File rootDir2;

    /**
     * construct with directory paths. Note it requires directory paths rather than file paths.
     */
    public CUMatcher(File f1, File f2) throws NotExistOrNotDirectoryException, ParserException {
        if (FileUtils.ifExistsAndDirectory(f1) && FileUtils.ifExistsAndDirectory(f2)) {
            rootDir1 = f1;
            rootDir2 = f2;
        } else {
            throw new NotExistOrNotDirectoryException("path1: " + f1.getPath() + "\npath2: " + f2.getPath());
        }
        this.match();
    }

    /**
     * constructor with paths of two directories
     * designed for the api with 2 directories
     *
     * @param dirPath1 directory path1
     * @param dirPath2 directory path2
     */
    public CUMatcher(String dirPath1, String dirPath2) throws NotExistOrNotDirectoryException, ParserException {
        this(new File(dirPath1), new File(dirPath2));
    }

    /**
     * using directories that have been initialized to gather files for MatchStore
     */
    public void match() throws ParserException {
        //TODO: maybe better to get a virtual file tree with only matched files
        recGetMatchedFile(rootDir1, rootDir2);
        //todo maybe adding bottom-up
    }

    /**
     * Get matched and unmatched files providing two corresponding dirs.<br/>
     * Only match those with the exact directory structure and the same filename.
     */
    private void recGetMatchedFile(File dir1, File dir2) throws ParserException {
        // top-down mapping for CUs
        Map<String, File> map1 = new HashMap<>();   //map1 can be deleted through optimization
        Map<String, File> map2 = new HashMap<>();

        //add to hashmap
        for (File f : Objects.requireNonNull(dir1.listFiles())) {
            map1.put(f.getName(), f);
        }
        for (File f : Objects.requireNonNull(dir2.listFiles())) {
            map2.put(f.getName(), f);
        }

        // look for matches
        // iterating in map1, note that do not delete elements in map1 while iterating, only map2 is ok to be modified
        for (String fileName : map1.keySet()) {
            if (map2.containsKey(fileName)) {
                // match
                File f1 = map1.get(fileName);
                File f2 = map2.get(fileName);
                map2.remove(fileName);
                if (f1.isFile() && f2.isFile()) {
                    if (FileUtils.isJavaFile(fileName)) {
                        CompilationUnit cu1 = javaParserUtils.file2CU(f1);
                        CompilationUnit cu2 = javaParserUtils.file2CU(f2);
                        if (cu1 != null && cu2 != null) {
                            cuMatchStore.getMatched().add(new Pair<>(new CUWithInfo(cu1, f1.getAbsolutePath()), new CUWithInfo(cu2, f2.getAbsolutePath())));
                        }
                    }
                } else if (f1.isDirectory() && f2.isDirectory()) {
                    recGetMatchedFile(f1, f2);
                } else {
                    // unmatch
                    logger.info("file and directory with the same name: " + map1.get(fileName).getAbsolutePath());
                    dealWithUnmatchInOriginalMap(map1.get(fileName));
                }
            } else {
                // unmatch
                // since dir and file with the same name can not coexist under the same dir
                dealWithUnmatchInOriginalMap(map1.get(fileName));
            }
        }

        // what's left in map2 is unmatched
        for (File file : map2.values()) {
            if (file.isDirectory()) {
                // dir
                for (File f : FileUtils.getAllFilesUnder(file)) {
                    if (FileUtils.isJavaFile(f)) {
                        cuMatchStore.getUnmatchedNodes2().add(new CUWithInfo(javaParserUtils.file2CU(f), f.getAbsolutePath()));
                    }
                }
            } else {
                // file
                if (FileUtils.isJavaFile(file)) {
                    cuMatchStore.getUnmatchedNodes2().add(new CUWithInfo(javaParserUtils.file2CU(file), file.getAbsolutePath()));
                }
            }
        }
        map1.clear();
        map2.clear();
    }

    private void dealWithUnmatchInOriginalMap(File f) throws ParserException {
        if (FileUtils.ifExistsAndDirectory(f)) {
            // directory
            FileUtils.getAllFilesUnder(f).forEach(child -> {
                // get all files that are not directories
                // all dirs are ignored
                if (FileUtils.isJavaFile(child)) {
                    // all java files under unmatched directory will be considered unmatched
                    // todo maybe bottom-up mapping?
                    try {
                        cuMatchStore.getUnmatchedNodes1().add(new CUWithInfo(javaParserUtils.file2CU(child), child.getAbsolutePath()));
                    } catch (ParserException e) {
                        e.printStackTrace();
                        //TODO 为什么不能直接throw？？？
                    }
                }
            });
        } else {
            // file
            if (FileUtils.isJavaFile(f)) {
                cuMatchStore.getUnmatchedNodes1().add(new CUWithInfo(javaParserUtils.file2CU(f), f.getAbsolutePath()));
            }
        }
    }

    public MatchStore<CUWithInfo> getCuMatchStore() {
        return cuMatchStore;
    }

}
